package no.nordicsemi.support.v18.scanner;

import ohos.utils.Parcel;
import ohos.utils.Sequenceable;

import java.io.Serializable;
import java.util.List;
/**
 * Bluetooth LE scan settings are passed to {@link BluetoothLeScannerCompat#startScan} to define the
 * parameters for the scan.
 */
public final class ScanSettings implements Serializable {
    public static final long MATCH_LOST_DEVICE_TIMEOUT_DEFAULT = 10000L; // [ms]
    public static final long MATCH_LOST_TASK_INTERVAL_DEFAULT = 10000L; // [ms]
    public static final int SCAN_MODE_OPPORTUNISTIC = -1;
    public static final int SCAN_MODE_LOW_POWER = 0;
    public static final int SCAN_MODE_BALANCED = 1;
    public static final int SCAN_MODE_LOW_LATENCY = 2;
    public static final int CALLBACK_TYPE_ALL_MATCHES = 1;
    public static final int CALLBACK_TYPE_FIRST_MATCH = 2;
    public static final int CALLBACK_TYPE_MATCH_LOST = 4;
    public static final int MATCH_NUM_ONE_ADVERTISEMENT = 1;
    public static final int MATCH_NUM_FEW_ADVERTISEMENT = 2;
    public static final int MATCH_NUM_MAX_ADVERTISEMENT = 3;
    public static final int MATCH_MODE_AGGRESSIVE = 1;
    public static final int MATCH_MODE_STICKY = 2;
    public static final int PHY_LE_ALL_SUPPORTED = 255;
    private final long powerSaveScanInterval;
    private final long powerSaveRestInterval;
    private int scanMode;
    private int callbackType;
    private long reportDelayMillis;
    private int matchMode;
    private int numOfMatchesPerFilter;
    private boolean isuseHardwareFilteringIfSupported;
    private boolean isuseHardwareBatchingIfSupported;
    private boolean isuseHardwareCallbackTypesIfSupported;
    private long matchLostDeviceTimeout;
    private long matchLostTaskInterval;
    private boolean legacy;
    private int phy;

    public int getScanMode() {
        return scanMode;
    }

    public int getCallbackType() {
        return callbackType;
    }

    public int getMatchMode() {
        return matchMode;
    }

    public int getNumOfMatches() {
        return numOfMatchesPerFilter;
    }

    public boolean getUseHardwareFilteringIfSupported() {
        return isuseHardwareFilteringIfSupported;
    }

    public boolean getUseHardwareBatchingIfSupported() {
        return isuseHardwareBatchingIfSupported;
    }

    public boolean getUseHardwareCallbackTypesIfSupported() {
        return isuseHardwareCallbackTypesIfSupported;
    }
    /**
     * but call {@link ScanCallback#onScanFailed(int)} with error = 5.
     * In that case the Scanner Compat will disable the hardware support and start using compat
     * mechanism.
     */
    /* package */
    void disableUseHardwareCallbackTypes() {
        isuseHardwareCallbackTypesIfSupported = false;
    }

    public long getMatchLostDeviceTimeout() {
        return matchLostDeviceTimeout;
    }

    public long getMatchLostTaskInterval() {
        return matchLostTaskInterval;
    }
    /**
     * Returns whether only legacy advertisements will be returned.
     * Legacy advertisements include advertisements as specified
     * by the Bluetooth core specification 4.2 and below.
     *
     * @return legacy
     */
    public boolean getLegacy() {
        return legacy;
    }
    /**
     * Returns the physical layer used during a scan.
     *
     * @return phy
     */
    public int getPhy() {
        return phy;
    }

    /**
     * Returns report delay timestamp based on the device clock.
     *
     * @return reportDelayMillis
     */
    public long getReportDelayMillis() {
        return reportDelayMillis;
    }

    private ScanSettings(final int scanMode, final int callbackType,
                         final long reportDelayMillis, final int matchMode,
                         final int numOfMatchesPerFilter, final boolean legacy, final int phy,
                         final boolean hardwareFiltering, final boolean hardwareBatching,
                         final boolean hardwareCallbackTypes, final long matchTimeout,
                         final long taskInterval,
                         final long powerSaveScanInterval, final long powerSaveRestInterval) {
        this.scanMode = scanMode;
        this.callbackType = callbackType;
        this.reportDelayMillis = reportDelayMillis;
        this.numOfMatchesPerFilter = numOfMatchesPerFilter;
        this.matchMode = matchMode;
        this.legacy = legacy;
        this.phy = phy;
        this.isuseHardwareFilteringIfSupported = hardwareFiltering;
        this.isuseHardwareBatchingIfSupported = hardwareBatching;
        this.isuseHardwareCallbackTypesIfSupported = hardwareCallbackTypes;
        this.matchLostDeviceTimeout = matchTimeout * 1000000L; // convert to nanos
        this.matchLostTaskInterval = taskInterval;
        this.powerSaveScanInterval = powerSaveScanInterval;
        this.powerSaveRestInterval = powerSaveRestInterval;
    }

    private ScanSettings(final Parcel in) {
        scanMode = in.readInt();
        callbackType = in.readInt();
        reportDelayMillis = in.readLong();
        matchMode = in.readInt();
        numOfMatchesPerFilter = in.readInt();
        legacy = in.readInt() != 0;
        phy = in.readInt();
        isuseHardwareFilteringIfSupported = in.readInt() == 1;
        isuseHardwareBatchingIfSupported = in.readInt() == 1;
        powerSaveScanInterval = in.readLong();
        powerSaveRestInterval = in.readLong();
    }


    public void writeToParcel(final Parcel dest, final int flags) {
        dest.writeInt(scanMode);
        dest.writeInt(callbackType);
        dest.writeLong(reportDelayMillis);
        dest.writeInt(matchMode);
        dest.writeInt(numOfMatchesPerFilter);
        dest.writeInt(legacy ? 1 : 0);
        dest.writeInt(phy);
        dest.writeInt(isuseHardwareFilteringIfSupported ? 1 : 0);
        dest.writeInt(isuseHardwareBatchingIfSupported ? 1 : 0);
        dest.writeLong(powerSaveScanInterval);
        dest.writeLong(powerSaveRestInterval);
    }


    public int describeContents() {
        return 0;
    }

    /**
     * Determine if we should do power-saving sleep on pre-Lollipop
     * @return boolean
     */
    public boolean hasPowerSaveMode() {
        return powerSaveRestInterval > 0 && powerSaveScanInterval > 0;
    }

    public long getPowerSaveRest() {
        return powerSaveRestInterval;
    }

    public long getPowerSaveScan() {
        return powerSaveScanInterval;
    }
    /**
     * Builder for {@link ScanSettings}.
     */
    public static final class Builder {
        private int scanMode = SCAN_MODE_LOW_POWER;
        private int callbackType = CALLBACK_TYPE_ALL_MATCHES;
        private long reportDelayMillis = 0;
        private int matchMode = MATCH_MODE_AGGRESSIVE;
        private int numOfMatchesPerFilter = MATCH_NUM_MAX_ADVERTISEMENT;
        private boolean legacy = true;
        private int phy = PHY_LE_ALL_SUPPORTED;
        private boolean isuseHardwareFilteringIfSupported = true;
        private boolean isuseHardwareBatchingIfSupported = true;
        private boolean useHardwareCallbackTypesIfSupported = true;
        private long matchLostDeviceTimeout = MATCH_LOST_DEVICE_TIMEOUT_DEFAULT;
        private long matchLostTaskInterval = MATCH_LOST_TASK_INTERVAL_DEFAULT;
        private long powerSaveRestInterval = 0;
        private long powerSaveScanInterval = 0;

        /**
         * Set scan mode for Bluetooth LE scan.
         * <p>
         * On Lollipop this mode will fall back {@link #SCAN_MODE_LOW_POWER}, which actually means
         * that the library will start its own scan instead of relying on scans from other apps.
         * This may have significant impact on battery usage.
         * <p>
         * On pre-Lollipop devices, the settings set by {@link #setPowerSave(long, long)}
         * will be used. By default, the intervals are the same as for {@link #SCAN_MODE_LOW_POWER}.
         *
         * @param scanMode The scan mode can be one of {@link ScanSettings#SCAN_MODE_LOW_POWER},
         *                 {@link #SCAN_MODE_BALANCED},
         *                 {@link #SCAN_MODE_LOW_LATENCY} or
         *                 {@link #SCAN_MODE_OPPORTUNISTIC}.
         * @throws IllegalArgumentException If the {@code scanMode} is invalid.
         *
         * @return Builder
         */
        public Builder setScanMode(final int scanMode) {
            if (scanMode < SCAN_MODE_OPPORTUNISTIC || scanMode > SCAN_MODE_LOW_LATENCY) {
                throw new IllegalArgumentException("invalid scan mode " + scanMode);
            }
            this.scanMode = scanMode;
            return this;
        }

        /**
         * Set callback type for Bluetooth LE scan.
         *
         * @param callbackType The callback type flags for the scan.
         * @throws IllegalArgumentException If the {@code callbackType} is invalid.
         *
         * @return Builder
         */

        public Builder setCallbackType(final int callbackType) {
            if (!isValidCallbackType(callbackType)) {
                throw new IllegalArgumentException("invalid callback type - " + callbackType);
            }
            this.callbackType = callbackType;
            return this;
        }

        /**
         * isValidCallbackType
         *
         * @param callbackType callbackType
         * @return boolean
         */
        private boolean isValidCallbackType(final int callbackType) {
            if (callbackType == CALLBACK_TYPE_ALL_MATCHES ||
                    callbackType == CALLBACK_TYPE_FIRST_MATCH ||
                    callbackType == CALLBACK_TYPE_MATCH_LOST) {
                return true;
            }
            return callbackType == (CALLBACK_TYPE_FIRST_MATCH | CALLBACK_TYPE_MATCH_LOST);
        }

        /**
         * Set report delay timestamp for Bluetooth LE scan.
         *
         * @param reportDelayMillis Delay of report in milliseconds. Set to 0 to be notified of
         *                          results immediately. Values &gt; 0 causes the scan results
         *                          to be queued up and delivered after the requested delay or
         *                          when the internal buffers fill up.<p>
         *                          For delays below 5000 ms (5 sec) the
         *                          {@link ScanCallback#onBatchScanResults(List)}
         *                          will be called in unreliable intervals, but starting from
         *                          around 5000 the intervals get even.
         * @throws IllegalArgumentException If {@code reportDelayMillis} &lt; 0.
         *
         * @return Builder
         */
        public Builder setReportDelay(final long reportDelayMillis) {
            if (reportDelayMillis < 0) {
                throw new IllegalArgumentException("reportDelay must be > 0");
            }
            this.reportDelayMillis = reportDelayMillis;
            return this;
        }

        /**
         * Set the number of matches for Bluetooth LE scan filters hardware match.
         *
         * @param numOfMatches The num of matches can be one of
         *                     {@link ScanSettings#MATCH_NUM_ONE_ADVERTISEMENT} or
         *                     {@link ScanSettings#MATCH_NUM_FEW_ADVERTISEMENT} or
         *                     {@link ScanSettings#MATCH_NUM_MAX_ADVERTISEMENT}
         * @throws IllegalArgumentException If the {@code matchMode} is invalid.
         *
         * @return Builder
         */

        public Builder setNumOfMatches(final int numOfMatches) {
            if (numOfMatches < MATCH_NUM_ONE_ADVERTISEMENT
                    || numOfMatches > MATCH_NUM_MAX_ADVERTISEMENT) {
                throw new IllegalArgumentException("invalid numOfMatches " + numOfMatches);
            }
            numOfMatchesPerFilter = numOfMatches;
            return this;
        }

        /**
         * Set match mode for Bluetooth LE scan filters hardware match
         *
         * @param matchMode The match mode can be one of
         *                  {@link ScanSettings#MATCH_MODE_AGGRESSIVE} or
         *                  {@link ScanSettings#MATCH_MODE_STICKY}
         * @throws IllegalArgumentException If the {@code matchMode} is invalid.
         *
         * @return Builder
         */

        public Builder setMatchMode(final int matchMode) {
            if (matchMode < MATCH_MODE_AGGRESSIVE
                    || matchMode > MATCH_MODE_STICKY) {
                throw new IllegalArgumentException("invalid matchMode " + matchMode);
            }
            this.matchMode = matchMode;
            return this;
        }

        /**
         * Set whether only legacy advertisements should be returned in scan results.
         * Legacy advertisements include advertisements as specified by the
         * Bluetooth core specification 4.2 and below. This is true by default
         * for compatibility with older apps.
         *
         * @param legacy true if only legacy advertisements will be returned
         *
         * @return Builder
         */
        public Builder setLegacy(final boolean legacy) {
            this.legacy = legacy;
            return this;
        }

        /**
         * Set the Physical Layer to use during this scan.
         * This is used only if {@link Builder#setLegacy}
         * may be used to check whether LE Coded phy is supported by calling
         * Selecting an unsupported phy will result in failure to start scan.
         * @param phy phy
         *
         * @return Builder
         */

        public Builder setPhy(final int phy) {
            this.phy = phy;
            return this;
        }

        /**
         * Several phones may have some issues when it comes to offloaded filtering.
         * Even if it should be supported, it may not work as expected.
         * It has been observed for example, that setting 2 filters with different devices
         * addresses on Nexus 6 with Lollipop gives no callbacks if one or both devices advertise.
         *
         * @param use true to enable (default) hardware offload filtering.
         *            If false a compat software filtering will be used
         *            (uses much more resources).
         *
         * @return Builder
         */

        public Builder setUseHardwareFilteringIfSupported(final boolean use) {
            isuseHardwareFilteringIfSupported = use;
            return this;
        }

        /**
         * Some devices, for example Samsung S6 and S6 Edge with Lollipop, return always
         * the same RSSI value for all devices if offloaded batching is used.
         * Batching may also be emulated using a compat mechanism - a periodically called timer.
         * Timer approach requires more resources but reports devices in constant delays
         * and works on devices that does not support offloaded batching.
         * In comparison, when setReportDelay(..) is called with parameter 1000 the standard,
         * hardware triggered callback will be called every 1500ms +-200ms.
         *
         * @param use true to enable (default) hardware offloaded batching if they are supported.
         *            False to always use compat mechanism.
         *
         * @return Builder
         */

        public Builder setUseHardwareBatchingIfSupported(final boolean use) {
            isuseHardwareBatchingIfSupported = use;
            return this;
        }

        /**
         * This method may be used when callback type is set to a value different than
         * {@link #CALLBACK_TYPE_ALL_MATCHES}. When disabled, the Scanner Compat itself will
         * take care of reporting first match and match lost. The compat behaviour may differ
         * <p>
         * Also, in compat mode values set by {@link #setMatchMode(int)} and
         * {@link #setNumOfMatches(int)} are ignored.
         * Instead use {@link #setMatchOptions(long, long)} to set timer options.
         *
         * @param use true to enable (default) the offloaded match reporting if hardware supports it,
         *            false to enable compat implementation.
         *
         * @return Builder
         */

        public Builder setUseHardwareCallbackTypesIfSupported(final boolean use) {
            useHardwareCallbackTypesIfSupported = use;
            return this;
        }

        /**
         * The match options are used when the callback type has been set to
         * {@link ScanSettings#CALLBACK_TYPE_FIRST_MATCH} or
         * {@link ScanSettings#CALLBACK_TYPE_MATCH_LOST} and hardware does not support those types.
         * In that case {@link BluetoothLeScannerCompat} starts a task that runs periodically
         * {@link #CALLBACK_TYPE_MATCH_LOST} if a device has not been seen for at least given time.
         *
         * @param deviceTimeoutMillis the time required for the device to be recognized as lost
         *                            (default {@link #MATCH_LOST_DEVICE_TIMEOUT_DEFAULT}).
         * @param taskIntervalMillis  the task interval (default {@link #MATCH_LOST_TASK_INTERVAL_DEFAULT}).
         *
         * @return Builder
         */

        public Builder setMatchOptions(final long deviceTimeoutMillis, final long taskIntervalMillis) {
            if (deviceTimeoutMillis <= 0 || taskIntervalMillis <= 0) {
                throw new IllegalArgumentException("maxDeviceAgeMillis and taskIntervalMillis must be > 0");
            }
            matchLostDeviceTimeout = deviceTimeoutMillis;
            matchLostTaskInterval = taskIntervalMillis;
            return this;
        }

        /**
         * setPowerSave
         *
         * @param scanInterval scanInterval
         * @param restInterval restInterval
         * @return Builder
         */
        public Builder setPowerSave(final long scanInterval, final long restInterval) {
            if (scanInterval <= 0 || restInterval <= 0) {
                throw new IllegalArgumentException("scanInterval and restInterval must be > 0");
            }
            powerSaveScanInterval = scanInterval;
            powerSaveRestInterval = restInterval;
            return this;
        }

        /**
         * build
         *
         * @return ScanSettings
         */
        public ScanSettings build() {
            if (powerSaveRestInterval == 0 && powerSaveScanInterval == 0)
                updatePowerSaveSettings();

            return new ScanSettings(scanMode, callbackType, reportDelayMillis, matchMode,
                    numOfMatchesPerFilter, legacy, phy, isuseHardwareFilteringIfSupported,
                    isuseHardwareBatchingIfSupported, useHardwareCallbackTypesIfSupported,
                    matchLostDeviceTimeout, matchLostTaskInterval,
                    powerSaveScanInterval, powerSaveRestInterval);
        }

        /**
         * Sets power save settings based on the scan mode selected.
         */
        private void updatePowerSaveSettings() {
            switch (scanMode) {
                case SCAN_MODE_LOW_LATENCY:
                    powerSaveScanInterval = 0;
                    powerSaveRestInterval = 0;
                    break;
                case SCAN_MODE_BALANCED:
                    powerSaveScanInterval = 2000;
                    powerSaveRestInterval = 3000;
                    break;
                case SCAN_MODE_OPPORTUNISTIC:
                case SCAN_MODE_LOW_POWER:
                default:
                    powerSaveScanInterval = 500;
                    powerSaveRestInterval = 4500;
                    break;
            }
        }
    }
}
